﻿/*
 * SonarAnalyzer for .NET
 * Copyright (C) 2014-2025 SonarSource Sàrl
 * mailto:info AT sonarsource DOT com
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the Sonar Source-Available License for more details.
 *
 * You should have received a copy of the Sonar Source-Available License
 * along with this program; if not, see https://sonarsource.com/license/ssal/
 */

using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using SonarAnalyzer.Core.AnalysisContext;
using SonarAnalyzer.CSharp.Core.Syntax.Utilities;
using CS = SonarAnalyzer.CSharp.Core.Extensions.SonarAnalysisContextExtensions;
using RoslynAnalysisContext = Microsoft.CodeAnalysis.Diagnostics.AnalysisContext;
using VB = SonarAnalyzer.VisualBasic.Core.Extensions.SonarAnalysisContextExtensions;

namespace SonarAnalyzer.Test.AnalysisContext;

public partial class SonarAnalysisContextTest
{
    private const string SnippetFileName = "snippet0.cs";
    private const string AnotherFileName = "Any other file name to make snippet0 considered as changed.cs";

    private static readonly ImmutableArray<DiagnosticDescriptor> DummyMainDescriptor = new[] { AnalysisScaffolding.CreateDescriptorMain() }.ToImmutableArray();

    [TestMethod]
    [DataRow(SnippetFileName, false)]
    [DataRow(AnotherFileName, true)]
    public void RegisterNodeAction_UnchangedFiles_SonarAnalysisContext(string unchangedFileName, bool expected)
    {
        var context = new DummyAnalysisContext(TestContext, unchangedFileName);
        var sut = new SonarAnalysisContext(context, DummyMainDescriptor);
        sut.RegisterNodeAction<SyntaxKind>(CSharpGeneratedCodeRecognizer.Instance, context.DelegateAction);

        context.AssertDelegateInvoked(expected);
    }

    [TestMethod]
    [DataRow("snippet1.cs")]
    [DataRow("Other file is unchanged.cs")]
    public void RegisterNodeActionInAllFiles_UnchangedFiles_GeneratedFiles_AlwaysRuns(string unchangedFileName) =>
        new VerifierBuilder<DummyAnalyzerForGenerated>()
            .WithAdditionalFilePath(AnalysisScaffolding.CreateSonarProjectConfigWithUnchangedFiles(TestContext, unchangedFileName))
            .AddSnippet("""
                        // <auto-generated/>
                        public class Something { } // Noncompliant
                        """)
            .Verify();

    [TestMethod]
    [DataRow(SnippetFileName, false)]
    [DataRow(AnotherFileName, true)]
    public void RegisterTreeAction_UnchangedFiles_SonarAnalysisContext(string unchangedFileName, bool expected)
    {
        var context = new DummyAnalysisContext(TestContext, unchangedFileName);
        var sut = new SonarAnalysisContext(context, DummyMainDescriptor);
        sut.RegisterTreeAction(CSharpGeneratedCodeRecognizer.Instance, context.DelegateAction);

        context.AssertDelegateInvoked(expected);
    }

    [TestMethod]
    [DataRow("", "", 0)]
    [DataRow("MainSourceScope", "", 1)]
    [DataRow("MainSourceScope", "//<auto-generated />", 0)]
    [DataRow("TestSourceScope", "", 0)]
    [DataRow("TestSourceScope", "//<auto-generated />", 0)]
    public async Task RegisterTreeAction_ScopeAndGeneratedCode(string scope, string autogenerated, int expected)
    {
        var snippet = new SnippetCompiler($$"""
            {{autogenerated}}
            class C
            {
                public void M() => ToString();
            }
            """);
        var treeContextExecuted = 0;
        var analyzer = new TestAnalyzerCS(AnalysisScaffolding.CreateDescriptor("TEST", scope), analysisContext =>
            analysisContext.RegisterTreeAction(CSharpGeneratedCodeRecognizer.Instance, treeContext =>
            {
                treeContextExecuted++;
            }));
        var compilation = snippet.Compilation.WithAnalyzers([analyzer]);
        var diagnostics = await compilation.GetAllDiagnosticsAsync();
        diagnostics.Should().BeEmpty();
        treeContextExecuted.Should().Be(expected);
    }

    [TestMethod]
    [DataRow("", "", 0, 0)]
    [DataRow("MainSourceScope", "", 1, 1)]
    [DataRow("MainSourceScope", "//<auto-generated />", 1, 0)]
    [DataRow("TestSourceScope", "", 0, 0)]
    [DataRow("TestSourceScope", "//<auto-generated />", 0, 0)]
    public async Task RegisterCompilationStartAction_RegisterTreeAction_ScopeAndGeneratedCode(string scope, string autogenerated, int expectedCompilationStartStartExecuted, int expectedTreeContextExecuted)
    {
        var snippet = new SnippetCompiler($$"""
            {{autogenerated}}
            class C
            {
                public void M() => ToString();
            }
            """);
        var compilationStartStartExecuted = 0;
        var treeContextExecuted = 0;
        var analyzer = new TestAnalyzerCS(AnalysisScaffolding.CreateDescriptor("TEST", scope), analysisContext =>
            analysisContext.RegisterCompilationStartAction(compilationStartContext =>
            {
                compilationStartStartExecuted++;
                compilationStartContext.RegisterTreeAction(CSharpGeneratedCodeRecognizer.Instance, treeContext =>
                {
                    treeContextExecuted++;
                });
            }));
        var compilation = snippet.Compilation.WithAnalyzers([analyzer]);
        var diagnostics = await compilation.GetAllDiagnosticsAsync();
        diagnostics.Should().BeEmpty();
        compilationStartStartExecuted.Should().Be(expectedCompilationStartStartExecuted);
        treeContextExecuted.Should().Be(expectedTreeContextExecuted);
    }

    [TestMethod]
    [DataRow(SnippetFileName, false)]
    [DataRow(AnotherFileName, true)]
    public void RegisterSemanticModelAction_UnchangedFiles_SonarAnalysisContext(string unchangedFileName, bool expected)
    {
        var context = new DummyAnalysisContext(TestContext, unchangedFileName);
        var sut = new SonarAnalysisContext(context, DummyMainDescriptor);
        sut.RegisterSemanticModelAction(CSharpGeneratedCodeRecognizer.Instance, context.DelegateAction);

        context.AssertDelegateInvoked(expected);
    }

    [TestMethod]
    public void RegisterSemanticModelAction_Extension_SonarAnalysisContext_CS()
    {
        var context = new DummyAnalysisContext(TestContext);
        var self = new SonarAnalysisContext(context, DummyMainDescriptor);
        CS.RegisterSemanticModelAction(self, context.DelegateAction);

        context.AssertDelegateInvoked(true);
    }

    [TestMethod]
    public void RegisterSemanticModelAction_Extension_SonarAnalysisContext_VB()
    {
        var context = new DummyAnalysisContext(TestContext);
        var self = new SonarAnalysisContext(context, DummyMainDescriptor);
        VB.RegisterSemanticModelAction(self, context.DelegateAction);

        context.AssertDelegateInvoked(true);
    }

    [TestMethod]
    [DataRow(SnippetFileName, false)]
    [DataRow(AnotherFileName, true)]
    public void RegisterCodeBlockStartAction_UnchangedFiles_SonarAnalysisContext(string unchangedFileName, bool expected)
    {
        var context = new DummyAnalysisContext(TestContext, unchangedFileName);
        var sut = new SonarAnalysisContext(context, DummyMainDescriptor);
        sut.RegisterCodeBlockStartAction<SyntaxKind>(CSharpGeneratedCodeRecognizer.Instance, context.DelegateAction);

        context.AssertDelegateInvoked(expected);
    }

    [TestMethod]
    public void SonarCompilationStartAnalysisContext_RegisterSemanticModel()
    {
        var context = new DummyAnalysisContext(TestContext);
        var startContext = new DummyCompilationStartAnalysisContext(context);
        var sut = new SonarCompilationStartAnalysisContext(new(context, DummyMainDescriptor), startContext);
        sut.RegisterSemanticModelActionInAllFiles(_ => { });

        startContext.AssertExpectedInvocationCounts(expectedSemanticModelCount: 1);
    }

    [TestMethod]
    public void SonarCompilationStartAnalysisContext_RegisterCompilationEndAction()
    {
        var context = new DummyAnalysisContext(TestContext);
        var startContext = new DummyCompilationStartAnalysisContext(context);
        var sut = new SonarCompilationStartAnalysisContext(new(context, DummyMainDescriptor), startContext);
        sut.RegisterCompilationEndAction(_ => { });

        startContext.AssertExpectedInvocationCounts(expectedCompilationEndCount: 1);
    }

    [TestMethod]
    public void SonarCompilationStartAnalysisContext_RegisterSymbolAction()
    {
        var context = new DummyAnalysisContext(TestContext);
        var startContext = new DummyCompilationStartAnalysisContext(context);
        var sut = new SonarCompilationStartAnalysisContext(new(context, DummyMainDescriptor), startContext);
        sut.RegisterSymbolAction(_ => { });

        startContext.AssertExpectedInvocationCounts(expectedSymbolCount: 1);
    }

    [TestMethod]
    public void SonarCompilationStartAnalysisContext_RegisterNodeAction()
    {
        var context = new DummyAnalysisContext(TestContext);
        var startContext = new DummyCompilationStartAnalysisContext(context);
        var sut = new SonarCompilationStartAnalysisContext(new(context, DummyMainDescriptor), startContext);
        sut.RegisterNodeAction<SyntaxKind>(CSharpGeneratedCodeRecognizer.Instance, _ => { });

        startContext.AssertExpectedInvocationCounts(expectedNodeCount: 1);
    }

    [TestMethod]
    public void SonarCompilationStartAnalysisContext_RegisterCompilationEnd_ReportsIssue()
    {
        var context = new DummyAnalysisContext(TestContext);
        var startContext = new DummyCompilationStartAnalysisContext(context);
        var sut = new SonarCompilationStartAnalysisContext(new(context, DummyMainDescriptor), startContext);
        var diagnostic = Diagnostic.Create(DiagnosticDescriptorFactory.CreateUtility("TEST", "Test report"), context.Tree.GetRoot().GetLocation());
        sut.RegisterCompilationEndAction(x => x.ReportIssue(CSharpGeneratedCodeRecognizer.Instance, diagnostic));

        startContext.RaisedDiagnostic.Should().NotBeNull().And.BeSameAs(diagnostic);
    }

    [TestMethod]
    public void SonarCompilationStartAnalysisContext_RegisterSymbol_ReportsIssue()
    {
        var context = new DummyAnalysisContext(TestContext);
        var startContext = new DummyCompilationStartAnalysisContext(context);
        var sut = new SonarCompilationStartAnalysisContext(new(context, DummyMainDescriptor), startContext);
        var diagnostic = Diagnostic.Create(DiagnosticDescriptorFactory.CreateUtility("TEST", "Test report"), context.Tree.GetRoot().GetLocation());
        sut.RegisterSymbolAction(x => x.ReportIssue(CSharpGeneratedCodeRecognizer.Instance, diagnostic));

        startContext.RaisedDiagnostic.Should().NotBeNull().And.BeSameAs(diagnostic);
    }

    [TestMethod]
    public void SonarCompilationStartAnalysisContext_RegisterSymbolStartAction()
    {
        var context = new DummyAnalysisContext(TestContext);
        var startContext = new DummyCompilationStartAnalysisContext(context);
        var sut = new SonarCompilationStartAnalysisContext(new(context, DummyMainDescriptor), startContext);
        var invocationCount = 0;
        sut.RegisterSymbolStartAction(x =>
        {
            x.Cancel.Should().Be(startContext.CancellationToken);
            x.Compilation.Should().Be(startContext.Compilation);
            x.Options.Should().Be(startContext.Options);
            x.Symbol.Should().NotBeNull();
            invocationCount++;
        }, SymbolKind.NamedType);
        startContext.RaisedDiagnostic.Should().BeNull();
        invocationCount.Should().Be(1);
    }

    [TestMethod]
    public void SonarCompilationStartAnalysisContext_RegisterCodeBlockStartAction_RegistrationIsCalled()
    {
        var context = new DummyAnalysisContext(TestContext);
        var startContext = Substitute.For<CompilationStartAnalysisContext>(context.Model.Compilation, context.Options, null);
        startContext.RegisterCodeBlockStartAction(Arg.Do<Action<CodeBlockStartAnalysisContext<SyntaxKind>>>(x =>
            x(context.CreateCodeBlockStartAnalysisContext<SyntaxKind>())));
        var sut = new SonarCompilationStartAnalysisContext(new(context, DummyMainDescriptor), startContext);
        var diagnostic = Diagnostic.Create(DiagnosticDescriptorFactory.CreateUtility("TEST", "Test report"), context.Tree.GetRoot().GetLocation());
        var registrationCalled = false;
        sut.RegisterCodeBlockStartAction<SyntaxKind>(CSharpGeneratedCodeRecognizer.Instance, x =>
        {
            registrationCalled = true;
            x.Model.Should().Be(context.Model);
            x.CodeBlock.Should().Be(context.ClassDeclarationNode);
            x.OwningSymbol.Should().Be(context.OwningSymbol);
        });

        registrationCalled.Should().BeTrue();
    }

    [TestMethod]
    [DataRow("", "", 0, 0)]
    [DataRow("MainSourceScope", "", 1, 1)]
    [DataRow("MainSourceScope", "//<auto-generated />", 1, 0)]
    [DataRow("TestSourceScope", "", 0, 0)]
    [DataRow("TestSourceScope", "//<auto-generated />", 0, 0)]
    public async Task SonarCompilationStartAnalysisContext_RegisterCodeBlockStart_ScopeAndGeneratedCode(string scope, string autogenerated, int expectedCompilationStartStartExecuted, int expectedCodeBlockStartExecuted)
    {
        var snippet = new SnippetCompiler($$"""
            {{autogenerated}}
            class C
            {
                public void M() => ToString();
            }
            """);
        var compilationStartStartExecuted = 0;
        var codeBlockStartExecuted = 0;
        var analyzer = new TestAnalyzerCS(AnalysisScaffolding.CreateDescriptor("TEST", scope), analysisContext =>
            analysisContext.RegisterCompilationStartAction(compilationStartContext =>
            {
                compilationStartStartExecuted++;
                compilationStartContext.RegisterCodeBlockStartAction<SyntaxKind>(CSharpGeneratedCodeRecognizer.Instance, codeBlockStart =>
                {
                    codeBlockStartExecuted++;
                });
            }));
        var compilation = snippet.Compilation.WithAnalyzers([analyzer]);
        var diagnostics = await compilation.GetAllDiagnosticsAsync();
        diagnostics.Should().BeEmpty();
        compilationStartStartExecuted.Should().Be(expectedCompilationStartStartExecuted);
        codeBlockStartExecuted.Should().Be(expectedCodeBlockStartExecuted);
    }

    [TestMethod]
    [DataRow("", "", 0)]
    [DataRow("MainSourceScope", "", 1)]
    [DataRow("MainSourceScope", "//<auto-generated />", 0)]
    [DataRow("TestSourceScope", "", 0)]
    [DataRow("TestSourceScope", "//<auto-generated />", 0)]
    public async Task SonarAnalysisContext_RegisterCodeBlockStart_ScopeAndGeneratedCode(string scope, string autogenerated, int expected)
    {
        var snippet = new SnippetCompiler($$"""
            {{autogenerated}}
            class C
            {
                public void M() => ToString();
            }
            """);
        var codeBlockStartExecuted = 0;
        var analyzer = new TestAnalyzerCS(AnalysisScaffolding.CreateDescriptor("TEST", scope), analysisContext =>
            analysisContext.RegisterCodeBlockStartAction<SyntaxKind>(CSharpGeneratedCodeRecognizer.Instance, codeBlockStart =>
                {
                    codeBlockStartExecuted++;
                }));
        var compilation = snippet.Compilation.WithAnalyzers([analyzer]);
        var diagnostics = await compilation.GetAllDiagnosticsAsync();
        diagnostics.Should().BeEmpty();
        codeBlockStartExecuted.Should().Be(expected);
    }

    [TestMethod]
    [DataRow("", "", 0)]
    [DataRow("MainSourceScope", "", 1)]
    [DataRow("MainSourceScope", "//<auto-generated />", 1)]
    [DataRow("TestSourceScope", "", 0)]
    [DataRow("TestSourceScope", "//<auto-generated />", 0)]
    public async Task SonarCompilationStartAnalysisContext_RegisterSymbolStartAction_ScopeAndGeneratedCode(string scope, string autogenerated, int expected)
    {
        var snippet = new SnippetCompiler($$"""
            {{autogenerated}}
            class C
            {
                public void M() => ToString();
            }
            """);
        var symbolStartExecuted = 0;
        var analyzer = new TestAnalyzerCS(AnalysisScaffolding.CreateDescriptor("TEST", scope), analysisContext =>
            analysisContext.RegisterCompilationStartAction(compilationStartContext =>
                compilationStartContext.RegisterSymbolStartAction(symbolStartContext =>
                {
                    symbolStartExecuted++;
                }, SymbolKind.NamedType)));
        var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create<DiagnosticAnalyzer>(analyzer));
        var diagnostics = await compilation.GetAllDiagnosticsAsync();
        diagnostics.Should().BeEmpty();
        symbolStartExecuted.Should().Be(expected);
    }

    [TestMethod]
    [DataRow("", "")]
    [DataRow("MainSourceScope", "", "Node", "CodeBlockStart_Node", "CodeBlock", "CodeBlockStart_End", "SymbolEnd")]
    [DataRow("MainSourceScope", "//<auto-generated />", "Node", "CodeBlockStart_Node", "CodeBlock", "CodeBlockStart_End")] // Note: "SymbolEnd" is missing here. ReportIssues does not forward the call.
                                                                                                                           // https://github.com/SonarSource/sonar-dotnet/issues/8876
    [DataRow("TestSourceScope", "")]
    [DataRow("TestSourceScope", "//<auto-generated />")]
    public async Task SonarCompilationStartAnalysisContext_RegisterSymbolStartAction_RegisterAndReporting_ScopeAndGeneratedCode(
        string scope, string autogenerated, params string[] expectedDiagnostics)
    {
        var snippet = new SnippetCompiler($$"""
            {{autogenerated}}
            class C
            {
                public void M() => ToString();
            }
            """);
        var diagnosticDescriptor = new DiagnosticDescriptor("TEST", "Title", "{0}", "Category", DiagnosticSeverity.Warning, true, customTags: [scope]);
        var location = Location.Create(snippet.SyntaxTree, TextSpan.FromBounds(0, 0));
        var analyzer = new TestAnalyzerCS(diagnosticDescriptor, analysisContext =>
            analysisContext.RegisterCompilationStartAction(compilationStartContext =>
                compilationStartContext.RegisterSymbolStartAction(symbolStartContext =>
                {
                    symbolStartContext.RegisterCodeBlockAction(codeBlockContext =>
                        codeBlockContext.ReportIssue(diagnosticDescriptor, location, "CodeBlock"));
                    symbolStartContext.RegisterCodeBlockStartAction<SyntaxKind>(codeBlockStartContext =>
                    {
                        codeBlockStartContext.RegisterNodeAction(nodeContext =>
                            nodeContext.ReportIssue(diagnosticDescriptor, location, "CodeBlockStart_Node"), SyntaxKind.InvocationExpression);
                        codeBlockStartContext.RegisterCodeBlockEndAction(codeBlockEndContext =>
                            codeBlockEndContext.ReportIssue(diagnosticDescriptor, location, "CodeBlockStart_End"));
                    });
                    symbolStartContext.RegisterSymbolEndAction(symbolEndContext =>
                        symbolEndContext.ReportIssue(CSharpGeneratedCodeRecognizer.Instance, diagnosticDescriptor, location, "SymbolEnd"));
                    symbolStartContext.RegisterSyntaxNodeAction(nodeContext =>
                        nodeContext.ReportIssue(diagnosticDescriptor, location, "Node"), SyntaxKind.InvocationExpression);
                },
            SymbolKind.NamedType)));
        var compilation = snippet.Compilation.WithAnalyzers([analyzer]);
        var diagnostics = await compilation.GetAllDiagnosticsAsync();
        diagnostics.Should().HaveCount(expectedDiagnostics.Length);
        // Ordering is only partially guaranteed and therefore we use BeEquivalentTo https://github.com/dotnet/roslyn/blob/main/docs/analyzers/Analyzer%20Actions%20Semantics.md
        diagnostics.Select(x => x.GetMessage()).Should().BeEquivalentTo(expectedDiagnostics);
    }

    [TestMethod]
    public async Task SonarCompilationStartAnalysisContext_RegisterSymbolStartAction_RegisterOperationAction_NotImplemented()
    {
        var snippet = new SnippetCompiler("""class C { }""");
        var analyzer = new TestAnalyzerCS(AnalysisScaffolding.CreateDescriptorMain(), analysisContext =>
            analysisContext.RegisterCompilationStartAction(compilationStartContext =>
                compilationStartContext.RegisterSymbolStartAction(symbolStartContext =>
                    symbolStartContext.RegisterOperationAction(_ => { }, ImmutableArray.Create(OperationKind.AddressOf)),
            SymbolKind.NamedType)));
        var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create<DiagnosticAnalyzer>(analyzer));
        var diagnostics = await compilation.GetAllDiagnosticsAsync();
        var ad0001 = diagnostics.Should().ContainSingle().Which;
        ad0001.Id.Should().Be("AD0001");
        ad0001.GetMessage().Should().Contain("'System.NotImplementedException' with message 'SonarOperationAnalysisContext wrapper type not implemented.'");
    }

    [TestMethod]
    public async Task SonarCompilationStartAnalysisContext_RegisterSymbolStartAction_RegisterOperationBlockAction_NotImplemented()
    {
        var snippet = new SnippetCompiler("""class C { }""");
        var analyzer = new TestAnalyzerCS(AnalysisScaffolding.CreateDescriptorMain(), analysisContext =>
            analysisContext.RegisterCompilationStartAction(compilationStartContext =>
                compilationStartContext.RegisterSymbolStartAction(symbolStartContext =>
                    symbolStartContext.RegisterOperationBlockAction(_ => { }),
            SymbolKind.NamedType)));
        var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create<DiagnosticAnalyzer>(analyzer));
        var diagnostics = await compilation.GetAllDiagnosticsAsync();
        var ad0001 = diagnostics.Should().ContainSingle().Which;
        ad0001.Id.Should().Be("AD0001");
        ad0001.GetMessage().Should().Contain("'System.NotImplementedException' with message 'SonarOperationBlockAnalysisContext wrapper type not implemented.'");
    }

    [TestMethod]
    public async Task SonarCompilationStartAnalysisContext_RegisterSymbolStartAction_RegisterOperationBlockStartAction_NotImplemented()
    {
        var snippet = new SnippetCompiler("""class C { }""");
        var analyzer = new TestAnalyzerCS(AnalysisScaffolding.CreateDescriptorMain(), analysisContext =>
            analysisContext.RegisterCompilationStartAction(compilationStartContext =>
                compilationStartContext.RegisterSymbolStartAction(symbolStartContext =>
                    symbolStartContext.RegisterOperationBlockStartAction(_ => { }),
            SymbolKind.NamedType)));
        var compilation = snippet.Compilation.WithAnalyzers(ImmutableArray.Create<DiagnosticAnalyzer>(analyzer));
        var diagnostics = await compilation.GetAllDiagnosticsAsync();
        var ad0001 = diagnostics.Should().ContainSingle().Which;
        ad0001.Id.Should().Be("AD0001");
        ad0001.GetMessage().Should().Contain("'System.NotImplementedException' with message 'SonarOperationBlockStartAnalysisContext wrapper type not implemented.'");
    }

#if NET

    [TestMethod]
    [DataRow("S109", "razor")]
    [DataRow("S103", "razor")]
    [DataRow("S1192", "razor")]
    [DataRow("S104", "razor")]
    [DataRow("S113", "razor")]
    [DataRow("S1451", "razor")]
    [DataRow("S1147", "razor")]
    [DataRow("S109", "cshtml")]
    [DataRow("S103", "cshtml")]
    [DataRow("S1192", "cshtml")]
    [DataRow("S104", "cshtml")]
    [DataRow("S113", "cshtml")]
    [DataRow("S1451", "cshtml")]
    [DataRow("S1147", "cshtml")]
    public void DisabledRules_ForRazor_DoNotRaise(string ruleId, string extension) =>
        new VerifierBuilder()
            .AddAnalyzer(() => new DummyAnalyzerWithLocation(ruleId, DiagnosticDescriptorFactory.MainSourceScopeTag))
            .WithAdditionalFilePath(AnalysisScaffolding.CreateSonarProjectConfig(TestContext, ProjectType.Product))
            .AddSnippet(Snippet(extension), $"SomeFile.{extension}")
            .VerifyNoIssues();

    [TestMethod]
    [DataRow("razor")]
    [DataRow("cshtml")]
    public void TestRules_ForRazor_DoNotRaise(string extension) =>
        new VerifierBuilder()
            .AddAnalyzer(() => new DummyAnalyzerWithLocation("DummyId", DiagnosticDescriptorFactory.TestSourceScopeTag))
            .WithAdditionalFilePath(AnalysisScaffolding.CreateSonarProjectConfig(TestContext, ProjectType.Test))
            .AddSnippet(Snippet(extension), $"SomeFile.{extension}")
            .VerifyNoIssues();

    [TestMethod]
    [DataRow("razor")]
    [DataRow("cshtml")]
    public void AllScopedRules_ForRazor_Raise(string extension)
    {
        var keyword = extension == "razor" ? "code" : "functions";
        new VerifierBuilder()
            .AddAnalyzer(() => new DummyAnalyzerWithLocation("DummyId", DiagnosticDescriptorFactory.TestSourceScopeTag, DiagnosticDescriptorFactory.MainSourceScopeTag))
            .WithAdditionalFilePath(AnalysisScaffolding.CreateSonarProjectConfig(TestContext, ProjectType.Product))
            .AddSnippet($$"""
                        @{{keyword}}
                        {
                            private int magicNumber = RaiseHere(); // Noncompliant
                            private static int RaiseHere()
                            {
                                return 42;
                            }
                        }
                        """,
                        $"SomeFile.{extension}")
            .Verify();
    }

    [TestMethod]
    [DataRow("razor")]
    [DataRow("cshtml")]
    public void RaisedIssue_WithinRazorGeneratedCode_ShouldNotBeReported(string extension) =>
        new VerifierBuilder()
            .AddAnalyzer(() => new DummyAnalyzerCS())
            .AddSnippet(@"<p>Some Html</p>", $"SomeFile.{extension}")
            .VerifyNoIssues();

    private static string Snippet(string extension)
    {
        var keyword = extension == "razor" ? "code" : "functions";
        return $$"""
                @{{keyword}}
                {
                    private int magicNumber = RaiseHere();
                    private static int RaiseHere()
                    {
                        return 42;
                    }
                }
                """;
    }

#endif

    private static CompilationStartAnalysisContext MockCompilationStartAnalysisContext(DummyAnalysisContext context)
    {
        var mock = Substitute.For<CompilationStartAnalysisContext>(context.Model.Compilation, context.Options, CancellationToken.None);
        mock.When(x => x.RegisterSyntaxNodeAction(Arg.Any<Action<SyntaxNodeAnalysisContext>>(), Arg.Any<ImmutableArray<SyntaxKind>>()))
            .Do(x => x.Arg<Action<SyntaxNodeAnalysisContext>>()(context.CreateSyntaxNodeAnalysisContext()));    // Invoke to call RegisterSyntaxTreeAction
        mock.When(x => x.RegisterSyntaxTreeAction(Arg.Any<Action<SyntaxTreeAnalysisContext>>()))
            .Do(x => x.Arg<Action<SyntaxTreeAnalysisContext>>()(new SyntaxTreeAnalysisContext(context.Tree, context.Options, _ => { }, _ => true, default)));
        mock.When(x => x.RegisterSemanticModelAction(Arg.Any<Action<SemanticModelAnalysisContext>>()))
            .Do(x => x.Arg<Action<SemanticModelAnalysisContext>>()(new SemanticModelAnalysisContext(context.Model, context.Options, _ => { }, _ => true, default)));
        mock.When(x => x.RegisterCodeBlockStartAction(Arg.Any<Action<CodeBlockStartAnalysisContext<SyntaxKind>>>()))
            .Do(x => x.Arg<Action<CodeBlockStartAnalysisContext<SyntaxKind>>>()(context.CreateCodeBlockStartAnalysisContext<SyntaxKind>()));
        return mock;
    }

    private sealed class DummyAnalysisContext : RoslynAnalysisContext
    {
        public readonly AnalyzerOptions Options;
        public readonly SemanticModel Model;
        public readonly SyntaxTree Tree;
        private bool delegateWasInvoked;

        public SyntaxNode ClassDeclarationNode => Tree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>().First();
        public ISymbol OwningSymbol => Model.GetDeclaredSymbol(ClassDeclarationNode);

        public DummyAnalysisContext(TestContext testContext, params string[] unchangedFiles)
        {
            Options = AnalysisScaffolding.CreateOptions(AnalysisScaffolding.CreateSonarProjectConfigWithUnchangedFiles(testContext, unchangedFiles));
            (Tree, Model) = TestCompiler.CompileCS("public class Sample { }");
        }

        public void DelegateAction<T>(T arg) =>
            delegateWasInvoked = true;

        public void AssertDelegateInvoked(bool expected, string because = "") =>
            delegateWasInvoked.Should().Be(expected, because);

        public SyntaxNodeAnalysisContext CreateSyntaxNodeAnalysisContext() =>
            new(Tree.GetRoot(), Model, Options, _ => { }, _ => true, default);

        public SemanticModelAnalysisContext CreateSemanticModelAnalysisContext() =>
            new(Model, Options, _ => { }, _ => true, default);

        public CodeBlockStartAnalysisContext<TSyntaxKind> CreateCodeBlockStartAnalysisContext<TSyntaxKind>()
            where TSyntaxKind : struct =>
            // SyntaxNode codeBlock, ISymbol owningSymbol, SemanticModel semanticModel, AnalyzerOptions options, CancellationToken cancellationToken
            Substitute.For<CodeBlockStartAnalysisContext<TSyntaxKind>>(ClassDeclarationNode, OwningSymbol, Model, Options, CancellationToken.None);

        public override void RegisterCodeBlockAction(Action<CodeBlockAnalysisContext> action) =>
            throw new NotImplementedException();

        public override void RegisterCodeBlockStartAction<TLanguageKindEnum>(Action<CodeBlockStartAnalysisContext<TLanguageKindEnum>> action) =>
            action(new DummyCodeBlockStartAnalysisContext<TLanguageKindEnum>(this));

        public override void RegisterCompilationAction(Action<CompilationAnalysisContext> action) =>
            throw new NotImplementedException();

        public override void RegisterCompilationStartAction(Action<CompilationStartAnalysisContext> action) =>
            action(MockCompilationStartAnalysisContext(this));  // Directly invoke to let the inner registrations be added into this.actions

        public override void RegisterSemanticModelAction(Action<SemanticModelAnalysisContext> action) =>
            action(CreateSemanticModelAnalysisContext());

        public override void RegisterSymbolAction(Action<SymbolAnalysisContext> action, ImmutableArray<SymbolKind> symbolKinds) =>
            throw new NotImplementedException();

        public override void RegisterSyntaxNodeAction<TLanguageKindEnum>(Action<SyntaxNodeAnalysisContext> action, ImmutableArray<TLanguageKindEnum> syntaxKinds) =>
            action(CreateSyntaxNodeAnalysisContext());

        public override void RegisterSyntaxTreeAction(Action<SyntaxTreeAnalysisContext> action) =>
            throw new NotImplementedException();
    }

    private class DummyCodeBlockStartAnalysisContext<TSyntaxKind> : CodeBlockStartAnalysisContext<TSyntaxKind> where TSyntaxKind : struct
    {
        public DummyCodeBlockStartAnalysisContext(DummyAnalysisContext baseContext) : base(baseContext.Tree.GetRoot(), null, baseContext.Model, baseContext.Options, default) { }

        public override void RegisterCodeBlockEndAction(Action<CodeBlockAnalysisContext> action) =>
            throw new NotImplementedException();

        public override void RegisterSyntaxNodeAction(Action<SyntaxNodeAnalysisContext> action, ImmutableArray<TSyntaxKind> syntaxKinds) =>
            throw new NotImplementedException();
    }

    private class DummyCompilationStartAnalysisContext : CompilationStartAnalysisContext
    {
        private readonly DummyAnalysisContext context;
        private int compilationEndCount;
        private int semanticModelCount;
        private int symbolCount;
        private int nodeCount;

        public Diagnostic RaisedDiagnostic { get; private set; }

        public DummyCompilationStartAnalysisContext(DummyAnalysisContext context) : base(context.Model.Compilation, context.Options, default) =>
            this.context = context;

        public void AssertExpectedInvocationCounts(int expectedCompilationEndCount = 0, int expectedSemanticModelCount = 0, int expectedSymbolCount = 0, int expectedNodeCount = 0)
        {
            compilationEndCount.Should().Be(expectedCompilationEndCount);
            semanticModelCount.Should().Be(expectedSemanticModelCount);
            symbolCount.Should().Be(expectedSymbolCount);
            nodeCount.Should().Be(expectedNodeCount);
        }

        public override void RegisterCodeBlockAction(Action<CodeBlockAnalysisContext> action) =>
            throw new NotImplementedException();

        public override void RegisterCodeBlockStartAction<TLanguageKindEnum>(Action<CodeBlockStartAnalysisContext<TLanguageKindEnum>> action) =>
            action(context.CreateCodeBlockStartAnalysisContext<TLanguageKindEnum>());

        public override void RegisterCompilationEndAction(Action<CompilationAnalysisContext> action)
        {
            compilationEndCount++;
            action(new CompilationAnalysisContext(context.Model.Compilation, context.Options, reportDiagnostic: x => RaisedDiagnostic = x, isSupportedDiagnostic: _ => true, default));
        }

        public override void RegisterSemanticModelAction(Action<SemanticModelAnalysisContext> action)
        {
            semanticModelCount++;
            action(new SemanticModelAnalysisContext(context.Model, context.Options, reportDiagnostic: x => RaisedDiagnostic = x, isSupportedDiagnostic: _ => true, default));
        }

        public override void RegisterSymbolAction(Action<SymbolAnalysisContext> action, ImmutableArray<SymbolKind> symbolKinds)
        {
            symbolCount++;
            action(new SymbolAnalysisContext(Substitute.For<ISymbol>(), context.Model.Compilation, context.Options, x => RaisedDiagnostic = x, _ => true, default));
        }

        public override void RegisterSyntaxNodeAction<TLanguageKindEnum>(Action<SyntaxNodeAnalysisContext> action, ImmutableArray<TLanguageKindEnum> syntaxKinds) =>
            nodeCount++;

        public override void RegisterSyntaxTreeAction(Action<SyntaxTreeAnalysisContext> action) =>
            throw new NotImplementedException();

        public override void RegisterSymbolStartAction(Action<SymbolStartAnalysisContext> action, SymbolKind symbolKind)
        {
            var symbolStartAnalysisContext = Substitute.For<SymbolStartAnalysisContext>(Substitute.For<ISymbol>(), context.Model.Compilation, context.Options, default);
            action(symbolStartAnalysisContext);
        }
    }

    [DiagnosticAnalyzer(LanguageNames.CSharp)]
    private class DummyAnalyzerForGenerated : SonarDiagnosticAnalyzer
    {
        private readonly DiagnosticDescriptor rule = AnalysisScaffolding.CreateDescriptorMain();

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(rule);

        protected override void Initialize(SonarAnalysisContext context) =>
            context.RegisterNodeActionInAllFiles(c => c.ReportIssue(rule, c.Node), SyntaxKind.ClassDeclaration);
    }

    [DiagnosticAnalyzer(LanguageNames.CSharp)]
    private sealed class TestAnalyzerCS(DiagnosticDescriptor rule, Action<SonarAnalysisContext> register) : SonarDiagnosticAnalyzer
    {
        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(rule);

        protected override void Initialize(SonarAnalysisContext context) =>
            register(context);
    }
}
